package com.yugao.fintech.framework.file.upload.local;

import cn.hutool.core.io.FileUtil;
import com.yugao.fintech.framework.assistant.core.file.FileUtils;
import com.yugao.fintech.framework.file.upload.exception.MergeChunksException;
import com.yugao.fintech.framework.file.upload.local.callback.LocalBreakpointResumeCallback;
import com.yugao.fintech.framework.file.upload.local.enums.FileUploadStateEnum;
import com.yugao.fintech.framework.file.upload.local.model.*;
import org.apache.commons.compress.utils.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.unit.DataSize;

import javax.annotation.Resource;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

@Component
public class LocalBreakpointResumeManagerImpl implements ILocalBreakpointResumeManager {
    private static final Logger log = LoggerFactory.getLogger(LocalBreakpointResumeManagerImpl.class);

    @Resource
    private UploadFilePathManager uploadFilePathManager;

    @Resource
    private LocalBreakpointResumeCallback uploadCallback;

    private static final String DEFAULT_CONTENT_TYPE = "application/octet-stream";
    public static final int BUF_SIZE = 1024 * 1204 * 20;
    public static long fileMaxByteSize = 0;

    @Override
    public BreakpointRegisterDTO register(BreakpointRegister breakpointRegister) {
        BreakpointRegisterDTO.BreakpointRegisterDTOBuilder resultBuilder = BreakpointRegisterDTO.builder();
        String fileCode = breakpointRegister.getFileCode();
        String fileExt = breakpointRegister.getFileExt();
        String fileName = breakpointRegister.getFileName();
        DataSize fileSize = breakpointRegister.getFileSize();
        String mergeFileFolderPath = uploadFilePathManager.getMergeFileFolderPath(fileCode);

        // 1. 检查文件是否存在于磁盘
        FileInfoOfFind fileInfoOfFind = uploadCallback.find(breakpointRegister);
        // 数据库中有该文件并且上传状态为已上传，返回给前端，实现秒传，直接返回resId、filePath
        if (fileInfoOfFind != null && FileUploadStateEnum.UPLOADED.equals(fileInfoOfFind.getState())) {
            resultBuilder.isUploaded(true).filePath(fileInfoOfFind.getFilePath()).fileId(String.valueOf(fileInfoOfFind.getId()));
            return resultBuilder.build();
        }

        // 数据库中不存在，则生成resource记录
        if (fileInfoOfFind == null) {
            // 首次断点续传的文件需要创建resource新记录
            FileRegisterInfo registerInfo = FileRegisterInfo.builder().fileCode(fileCode)
                    .fileExt(fileExt).uploadStatus(FileUploadStateEnum.NOT_UPLOAD_COMPLETED) // 没有上传
                    .fileSize(fileSize).fileName(fileName).ext(breakpointRegister.getExt())
                    .build();
            uploadCallback.save(registerInfo);
        }

        // 若文件不存在则检查文件所在目录是否存在
        File fileFolder = new File(mergeFileFolderPath);
        if (!fileFolder.exists()) {
            // 不存在创建该目录 （目录就是根据前端传来的MD5值创建的）
            FileUtils.mkdirs(fileFolder);
        }
        return resultBuilder.isUploaded(false).build();
    }

    @Override
    public UploadChunkDTO uploadChunk(UploadChunk uploadChunk) {
        UploadChunkDTO result = UploadChunkDTO.builder().build();
        // 检查分块目录是否存在
        String chunkFileFolderPath = uploadFilePathManager.getChunkFileFolderPath(uploadChunk.getFileCode());
        File chunkFileFolder = new File(chunkFileFolderPath);
        FileUtils.mkdirs(chunkFileFolder);
        // 上传文件输入流
        File chunkFile = new File(chunkFileFolderPath + uploadChunk.getChunk());
        try (InputStream inputStream = uploadChunk.getInputStream(); FileOutputStream outputStream = new FileOutputStream(chunkFile)) {
            IOUtils.copy(inputStream, outputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return result;
    }

    @Override
    public boolean checkChunk(String fileCode, Integer chunk, Integer chunkSize) {
        // 检查分块文件是否存在
        String chunkFileFolderPath = uploadFilePathManager.getChunkFileFolderPath(fileCode);
        // 分块所在路径+分块的索引可定位具体分块
        File chunkFile = new File(chunkFileFolderPath + chunk);
        return chunkFile.exists() && chunkFile.length() == chunkSize;
    }

    @Override
    public MergeChunksDTO mergeChunks(MergeChunks mergeChunks) throws Exception {
        mergeChunks.check();
        String fileExt = mergeChunks.getFileExt();
        String fileCode = mergeChunks.getFileCode();
        String fileName = mergeChunks.getFileName();
        String chunkFileFolderPath = this.uploadFilePathManager.getChunkFileFolderPath(fileCode);
        String mergeFilePath = this.uploadFilePathManager.getMergeFilePath(fileCode, fileExt);
        mergeChunks.setFilePath(mergeFilePath);
        mergeChunks.setChunkPath(chunkFileFolderPath);
        mergeChunks.setContentType(DEFAULT_CONTENT_TYPE);

        // 校验本地分片文件是否存在，如果不存在则证明 fileCode 值是非法的
        File chunkFileFolder = new File(chunkFileFolderPath);
        if (!chunkFileFolder.exists()) {
            throw new MergeChunksException("非法 fileCode 值");
        }

        File[] chunkFileArr = chunkFileFolder.listFiles();
        File mergeFile = new File(mergeFilePath);
        assert chunkFileArr != null;
        List<File> chunkFileList = Arrays.stream(chunkFileArr).collect(Collectors.toList());

        boolean existFile = uploadCallback.exist(mergeChunks);
        if (!existFile) {
            log.error("合并失败，没有从数据库中找到对应上传记录");
            throw new MergeChunksException("合并失败, 未查找到上传记录");
        }

        // 对分片排序
        this.sort(chunkFileList);
        int chunks = getChunks(chunkFileList);
        mergeChunks.setChunkFiles(chunkFileList);
        mergeChunks.setChunks(chunks);
        if (chunks <= 0) {
            throw new MergeChunksException("请上传分片");
        }

        // 合并之前
        boolean isMergeFile = uploadCallback.mergeBefore(mergeChunks);

        // 判断是否需要合并分片
        if (isMergeFile) {
            log.debug("开始合并文件 => {}", fileName);
            this.mergeFile(chunkFileList, mergeFile);
        }
        // 合并之后
        MergeChunksDTO mergeChunksDTO = uploadCallback.mergeAfter(mergeChunks);
        // 删除文件分块目录下的所有分块
        FileUtil.del(chunkFileFolderPath);
        log.info("文件合并完成 => {}", fileName);
        return mergeChunksDTO;
    }

    /**
     * 对文件列表按照文件名数字大小进行排序
     *
     * @param fileList
     */
    private void sort(List<File> fileList) {
        // 排序
        fileList.sort((o1, o2) -> {
            if (Integer.parseInt(o1.getName()) > Integer.parseInt(o2.getName())) {
                return 1;
            }
            return -1;
        });
    }

    /**
     * 合并文件
     * 开启多线程进行合并
     *
     * @return java.io.File
     */
    private void mergeFile(List<File> chunkFileList, File mergeFile) {
        try {
            // 有删 无创建
            if (mergeFile.exists()) {
                mergeFile.delete();
            } else {
                mergeFile.createNewFile();
            }
            mergeFileWithNio(chunkFileList, mergeFile);
        } catch (Exception e) {
            log.error("error: ", e);
            throw new MergeChunksException("合并失败,请联系管理员");
        }
    }

    /**
     * 合成方法：NIO方式
     *
     * @param files   要合并的文件
     * @param newFile 新文件
     */
    public static void mergeFileWithNio(List<File> files, File newFile) {
        FileChannel outChannel = null;
        FileChannel inChannel = null;
        int chunkCount = getChunks(files);
        try (FileOutputStream outputStream = new FileOutputStream(newFile);) {
            outChannel = outputStream.getChannel();
            for (int i = 0; i < chunkCount; i++) {
                File file = files.get(i);
                log.info("正在合并分片 [{}], 分片大小: {}", file.getName(), file.length());
                try (FileInputStream inputStream = new FileInputStream(file)) {
                    inChannel = inputStream.getChannel();
                    ByteBuffer bb = ByteBuffer.allocate(BUF_SIZE);
                    while (inChannel.read(bb) != -1) {
                        bb.flip();
                        outChannel.write(bb);
                        bb.clear();
                    }
                    inChannel.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            log.debug("文件合并成功 fileName: {}, fileSize: {}", newFile.getName(), newFile.length());
        } catch (IOException e) {
            log.error("error: ", e);
            throw new MergeChunksException("合并失败,请联系管理员");
        } finally {
            try {
                if (outChannel != null) {
                    outChannel.close();
                }
                if (inChannel != null) {
                    inChannel.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 获取分片数量
     */
    public static int getChunks(List<File> files) {
        long fileTotalSize = 0;
        int chunkCount = 0;

        // 获取需要合并的文件大小
        for (File chunkFile : files) {
            fileTotalSize = fileTotalSize + chunkFile.length();
            if (fileTotalSize > fileMaxByteSize) {
                log.warn("客户端在注册文件时候，文件大小超过最大值，文件不会被合并且分片将会被删除");
                chunkCount = 0;
                break;
            }
            chunkCount += 1;
        }
        log.info("分片数量为 [{}]", chunkCount);
        return chunkCount;
    }
}
